Skip to content

feat(spec): $icontains(ASCII 折叠域)+ $contains 族钉死大小写敏感 + $regex 退役指引表 (#5701) - #5752

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5701-regex-retire-icontains
Aug 6, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-5701-regex-retire-icontains

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5701

#4706 维护者裁决 B 案的契约半边。纯声明 + case-set + DEBT 台账,不引入任何活体拒收,不改任何驱动代码 —— 五个后端今天怎么答,落地后还怎么答。

裁定摘录(#5701 的 2026-08-06「维护者裁决转录」)

裁定 本 PR 的落点
Q1 A —— $icontains 折叠域 = ASCII-only(A-Z/a-z),明写进 zod .describe() 与文档,边界显著标注 StringOperatorSchema.$icontains 的 .describe() + 文档 Callout(café 不匹配 CAFÉ)
Q2 A —— $contains / $startsWith / $endsWith 同批钉死为大小写敏感;filter.zod.ts:139 那句「backend level」按 PD #13 留明写取代记录 StringOperatorSchema 文档块「This SUPERSEDES a recorded decision」:原句原样引用 + 五面实测分歧表
Q3 B —— spec 只声明数据;新建 RETIRED_FILTER_OPERATORS,不收窄 FilterConditionSchema,不引入活体拒收 filter.zod.ts 新增纯数据表($regex / $options),Object.freeze,处方点名 $icontains
Q4 B —— 新建姊妹 case-set,expectRejection 判别式在新文件定义,五 driver 记实测 DEBT,main 不红 新增 packages/spec/src/data/filter-text-conformance.ts + 门禁 5 条 DEBT
附 $istartsWith / $iendsWith 本批不加 未加

用例面对照表(派发要求 6 条 → FILTER_TEXT_CASES)

# 派发要求 落地用例 期望
1 $icontains ASCII 双向折叠命中 ...matches an upper-case row from a lower-case comparand / ...from an upper-case comparand 均 ['1','2'],两向对称
2 ASCII-only 域 pin ASCII-only: a lower-case non-ASCII comparand does NOT match its upper-case row + 镜像 ['4'] / ['3'] —— 全 Unicode 折叠两向都返回 ['3','4'],两向都错
3 % / _ / 正则元字符作字面量 $icontains treats % / _ / . as a literal ... + $contains treats _ as a literal character too ['5'] / ['7'] / ['9'] / ['7']
4 空串 / 非字符串比较值拒收(ADR-0112) an empty $icontains comparand is REFUSED / a non-string ... is REFUSED expectRejection,code: 'INVALID_FILTER'
5 Q2=A pin:$contains 大小写敏感 $contains is case-SENSITIVE ×2、$startsWith、$endsWith、$notContains ['2'] / ['1'] / ['1'] / ['2'] / 8 行
6 $regex 拒收 pin,信息含 $icontains $regex is REFUSED, and the refusal names $icontains、$regex with $options、悬空 $options expectRejection + mustMention 含 $icontains

九行 fixture 成对设计(ASCII 大小写 / 非 ASCII 大小写 / % / _·.),每对只差一个受测字符,所以错答返回的是看得见的错 id,而不是碰巧相同的行数。

filter-text-conformance.test.ts 用一个按声明语义写的参考求值器把整张表跑了一遍 —— 表里每个 expected 都由 ASCII 折叠语义复算出来,不是手数的;另外两条断言证明这张表非空洞:全 Unicode 折叠必须恰好在那两条 ASCII-only 用例上给出不同答案。

⚠️ 一处判断点:$icontains 暂不进 FILTER_OPERATORS(请重点审这段)

派发口径是「$icontains 入词表」。实施时实测发现 FILTER_OPERATORS 不是词表,是运行时白名单,有两个消费者从它派生而非重述:

  • driver-memory 的 SUPPORTED_FIELD_OPERATORS = new Set([...FILTER_OPERATORS, '$regex', '$options']),是它形状门禁的接受集合;而 memory-matcher 的 default: 分支是 break(注释写明「Unreachable through match: the shape gate refuses an operator this driver does not evaluate」)。
  • service-analytics 的 objectql-echo-operator-coverage.test.ts 断言其编译器对每个成员都渲染出谓词。

实测(先写预测,再跑):把 $icontains 加进 FILTER_OPERATORS、重建 spec dist 后,在 driver-memory 内探针得到:

SUPPORTED_FIELD_OPERATORS.has('$icontains') = true
gate: NONE — ACCEPTED BY THE GATE
match({name:'zzz'}, {name:{$icontains:'acme'}}) = true   // 谓词被静默丢弃

一行不该命中的记录命中了。谓词被丢不是收窄而是放大,在 RLS 读作用域上是越权读(#3948),且发生在任何后端能回答该算子之前。这与派发的三条硬约束(不动驱动代码 / 不引入活体行为 / main 不红)直接冲突。

因此本 PR 的落点:$icontains 声明在 StringOperatorSchema / FieldOperatorsSchema / Filter< T >(已核实这三处没有任何运行时白名单消费者 —— NormalizedFilterSchema 是它们唯一的消费者,而运行时无人用它 parse filter,故此处声明是惰性的),FILTER_OPERATORS 保持不变,并在其文档块写明这条过渡与解除条件。

这个差异不是无声的:filter-operator-vocabulary.test.ts 把「声明面 − 执行面」钉死为恰好 { $icontains },双向断言 ——

若维护者认为应当强行入表并同批修 driver-memory,请驳回本段,我按新口径重做(那会把本单从「零驱动改动」变成跨 #5702 领地)。

反向验证:预测 vs 实测

# 事先预测 实测
P1 入表后 driver-memory 门禁接受 $icontains has() = true,门禁无拒收 ✅
P2 接受后匹配器 default: break 使谓词被丢 → 放大 不该命中的行返回 true ✅
P3 service-analytics 算子覆盖门禁变红 前两轮测量作废,第三轮才成立 —— 见下 ✅(重测后)
P4 不入表则五驱动在 FILTER_TEXT 列全判 DEBT(非红) 矩阵 25 covered / 5 DEBT / 0 exempt,门禁 OK ✅

全仓 $regex 清点 —— 无一处需要翻转,逐处说明

派发要求「examples / 测试 / 文档中现存 $regex 用法随本 PR 翻转」。实测结论:这三类里没有一处「使用」 —— 全是拒收 pin、历史注释,或明确划归他单的活体。逐处:

位置 性质 处置
examples/**、skills/** 零命中 ——
content/docs/** 零处 $regex 过滤用法;唯一相关是 search 展开那句「in-memory driver matches with a case-insensitive regex」 已改:改写为新契约 + 现状偏差 Callout 指向 #5702
service-analytics 的 read-scope-sql / 两处 *-not-null-safe 测试 用 $regex 作「编译器必须拒收的算子」,本身就是拒收 pin 保留(翻成 $icontains 会毁掉断言意图)
service-analytics 两处 objectql-*operator*.test.ts #5557 历史注释 + 一个刻意模拟 driver-memory $regex 分支的替身引擎 保留,随 #5702 该分支消失而去
packages/rest/analytics-filter-refusal-envelope.test.ts:292 钉死 read-scope-sql 对 $regex 的拒收报文 保留(已是拒收 pin)
packages/formula/matches-filter.test.ts:95 用 $regex 作 fail-closed 的「未知算子」样本 保留(退役后它「更是」未知算子)
plugin-auth:1 生产者 + 3 测试替身 全仓唯一活生产者,在认证路径上 ⛔ #5710
objectql/having-filter.ts(+test)、四个驱动 消费者 ⛔ #5702
.changeset/*.md 四份 待发布的历史 changeset 不改写既成记录

不触清单(派发 ⛔ 项,已逐条核对)

plugin-auth/objectql-adapter.ts(#5710)、driver-sql / driver-turso / driver-memory / objectql having-filter(#5702)、component.zod / hook.zod / api-routes / kernel-contracts、content/docs/releases/。

conversions/registry.ts 未触发 —— 退役的不是 authorable key(spec 从未声明 $regex),ADR-0087 D2 无对象可登记,与上轮结论一致。#4923 无冲突(其 #5731 已合入,本分支已 merge)。

一处需要更正上轮口径:StringOperatorSchema 确实在 authorable-surface 轨道上,gen:schema 因此新增了恰好一个键 data/StringOperator:$icontains(纯新增,删除门禁不涉及),已随本 PR 提交。FieldOperatorsSchema 不在该轨道(全文件零命中),所以那一处声明不产生生成物变化。

顺带记录的两处实测(不在本 PR 修,已附着到 #5702)

  1. driver-memory 的 $contains 有两种大小写答案:查询路径 / 分析面走 new RegExp(escapeRegex(v), 'i')(不敏感、全 Unicode),参考匹配器 memory-matcher.match() 走 includes(敏感)。上轮调研只读到后者,本 PR 的 DEBT 条目已按实测改写。
  2. driver-mongodb 的未知算子拒收逃出 ADR-0112 信封:default: 抛裸 new Error,而同文件的 unsupportedFilterError(另三处在用)才带 INVALID_FILTER / 400。

两条都写进了 DEBT 台账条目正文(实施者读台账即可看到),并在 #5702 留了评论。

验证输出(均在合入最新 origin/main 之后重跑)

check:generated        ✓ All 10 generated artifacts are up to date.
                         (合并后 os-regen 驱动挂起 api-surface.json / authorable-surface.json,
                          --fix 只重生成这两个;authorable-surface.base.json 不动 ——
                          它是删除门禁的锚点,main 只在「删键」时前移,本单只加键,
                          check:authorable-surface 原话:trails the merge base by 5 key(s) — expected)

check:driver-conformance
  OK  self-test: detects driven / unused / re-declared fixtures, discovers both axes,
      and holds the dead-root hard error
  driver conformance matrix (5 drivers x 6 case-sets) —— FILTER_TEXT 列五格全 DEBT,其余全 ok
  check-driver-conformance: OK — 25 covered cell(s), 5 in the DEBT ledger, 0 exempt.

pnpm --filter @objectstack/spec test
  Test Files  319 passed (319)
       Tests  8136 passed (8136)

pnpm --filter @objectstack/spec typecheck
  tsc --noEmit ✓
  check:test-typecheck: OK — @objectstack/spec's test layer compiles

P3 反向验证(重测,红→绿闭环)

# 反事实:$icontains 进 FILTER_OPERATORS,重建 spec dist
PATCH add: applied
--- Failed Tests 2 ---
 FAIL  ... > covers every operator the spec declares — no case is missing from the table
   AssertionError: expected [ '$between', '$contains', ...(13) ] to deeply equal [ ...(14) ]
 FAIL  ... > $icontains renders a predicate in the echo
   AssertionError: $icontains echoed no WHERE — the predicate was dropped
 Test Files  1 failed (1)      Tests  2 failed | 24 passed (26)

# 撤销 + 字节级校验 + 重建
PATCH remove: applied
RESTORED byte-identical
 Test Files  1 passed (1)      Tests  25 passed (25)

比预测更强:我只预测了词表等值断言变红(第 1 条)。实测多出第 2 条 —— $icontains echoed no WHERE — the predicate was dropped,即 analytics 编译器也把这个谓词静默丢掉了。这是在第二个消费者身上独立复现了与 driver-memory 同一个「放大」缺陷,而不是同一处的两种说法。

过程更正(如实记录):这个测量的前两轮都作废过。第一轮两臂同红(模块收集期失败,不是断言失败),原因是新工作树没预构建 service-analytics 依赖链 —— 失败形态与「你的改动弄坏了 import」一模一样;若没有事先设好对照臂,这轮会被误读成 P3 已验证。第二轮反事实根本没被应用:perl -0pi -e "s/\Q$WITHOUT\E/.../" 里 \Q 只关元字符、不关插值,$contains / $notContains 被 perl 当成自己的变量插成空串,两臂又都成了对照。第三轮改用 node 做替换,并把「已应用 / 已还原」从假设改成断言(出现次数必须恰为 1,还原后必须与备份字节相同),才拿到上面的红绿闭环。


Generated by Claude Code

claude added 3 commits August 6, 2026 03:27
… case-sensitive, retire $regex (#5701)

The contract half of #4706's maintainer ruling (B). Declaration only: no live
rejection, no driver code touched, every backend answers exactly what it
answered before.

- `$icontains` on StringOperatorSchema / FieldOperatorsSchema / Filter: contains,
  ignoring ASCII case (A-Z against a-z) and nothing else. The boundary is stated
  for authors — `café` does NOT match `CAFÉ` — because ASCII is the one fold all
  five backends can deliver: SQLite without ICU (so turso and sqlite-wasm too)
  folds ASCII only, and promising Unicode would repeat the defect this retires.

- `$contains` / `$notContains` / `$startsWith` / `$endsWith` are declared
  CASE-SENSITIVE, superseding the recorded "Case sensitivity should be handled at
  backend level" (Prime Directive #13 — the old sentence is quoted in place so
  the reversal is findable from the sentence a reader remembers). What that
  non-guarantee bought, measured: three different answers across five backends,
  two of them inside driver-memory alone (query path folds full Unicode, the
  reference matcher is case-sensitive).

- `RETIRED_FILTER_OPERATORS`: pure data, no behaviour. `$regex` and `$options`
  with prescriptions naming `$icontains`. Nothing rejects them here — the five
  existing refusal sites are wired to it by #5702, and #5710 must flip the one
  live producer (plugin-auth's ObjectQL adapter, on the authentication path)
  first or sign-in breaks.

- `FILTER_TEXT_CASES` (`data/filter-text-conformance.ts`): the shared standard
  for case folding, literal comparands and the `$regex` refusal, with the
  `expectRejection` discriminant `filter-logic-conformance.ts` deliberately never
  grew. A sibling table rather than rows in that one, whose charter excludes both
  axes and warns against red rows for work nobody is dispatched to do. Registered
  in check:driver-conformance with one MEASURED DEBT row per driver pointing at
  #5702 — 25 covered / 5 DEBT / 0 exempt, main stays green.

`$icontains` is deliberately NOT in `FILTER_OPERATORS` yet. That array is a
runtime allowlist, not a word list: driver-memory's shape gate derives its
ACCEPTED set from it while its matcher's `default:` arm breaks. Measured by
adding it early and rebuilding — the gate stopped refusing and
`match({name:'zzz'}, {name:{$icontains:'acme'}})` returned true, i.e. the
predicate was silently dropped and every row matched. A dropped predicate widens
rather than narrows, which on an RLS read scope is a permission bypass (#3948).
It joins the array in the PR that implements it; `filter-operator-vocabulary.test.ts`
pins the difference at exactly {$icontains} so neither adding a second staged
operator nor clearing this one can happen silently.

Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D

Co-authored-by: Claude <noreply@anthropic.com>
…g main

The merge driver (`merge=os-regen`, AGENTS.md §11) deferred both artifacts
rather than text-merging them, and `pre-commit` collects that debt. Regenerated
from the MERGED source, so the union includes #5721's ActionSession entries
alongside this branch's $icontains / FILTER_TEXT additions.

`authorable-surface.base.json` is deliberately NOT bumped: it is the deletion
gate's anchor, main only moves it when a key is REMOVED, and this branch only
adds. `check:authorable-surface` confirms it — 'trails the merge base by 5
key(s) — expected right after a surface change lands'.

Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:57am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: $regex 按 ADR-0049 退役 + $icontains 入算子词表与 FILTER_LOGIC_CASES(#4706 裁决 B 案 · 契约半边,先行)

2 participants